package io.kiki.sba.registry.server.integration;

import io.kiki.sba.registry.api.Channel;
import io.kiki.sba.registry.common.model.Response;
import io.kiki.sba.registry.common.model.store.Url;
import io.kiki.sba.registry.core.model.Result;
import io.kiki.sba.registry.remoting.http.HttpClient;
import io.kiki.sba.registry.server.clientInterface.ClientInterfaceApplication;
import io.kiki.sba.registry.server.data_store.DataStoreApplication;
import io.kiki.sba.registry.server.data_store.bootstrap.Bootstrap;
import io.kiki.sba.registry.server.metadata.MetadataApplication;
import io.kiki.sba.registry.server.metadata.bootstrap.MetaServerBootstrap;
import io.kiki.sba.registry.util.FileUtils;
import io.kiki.sba.registry.util.StringFormatter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.util.Strings;
import org.h2.tools.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;

public class RegistryApplication {
    private static final Logger logger = LoggerFactory.getLogger(RegistryApplication.class);

    private static final String META_HTTP_SERVER_PORT = "meta.server.httpServerPort";
    private static final String DATA_HTTP_SERVER_PORT = "data.server.httpServerPort";
    private static final String SESSION_HTTP_SERVER_PORT = "session.server.httpServerPort";

    private static final String H2_DRIVER = "org.h2.Driver";

    private static ConfigurableApplicationContext metaApplicationContext;
    private static ConfigurableApplicationContext sessionApplicationContext;
    private static ConfigurableApplicationContext dataApplicationContext;

    public static void main(String[] args) throws Exception {
        if (StringUtils.isBlank(System.getProperty("spring.profiles.active"))) {
            System.setProperty("spring.profiles.active", "dev");
        }
        System.setProperty("registry.lease.duration.secs", "10");
        System.setProperty("registry.elector.warm.up.millis", "2000");

        // setup DefaultUncaughtExceptionHandler
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> logger.error("UncaughtException in Thread({}): {}", t.getName(), e.getMessage(), e));

        // start registry application
        ConfigurableApplicationContext commonContext = new SpringApplicationBuilder(RegistryApplication.class).run(args);

        Collection<String> serverList = Collections.singletonList("localhost");

        String driver = commonContext.getEnvironment().getProperty("jdbc.driverClassName");
        if (H2_DRIVER.equals(driver)) {
            createTables(commonContext);
            Server.createWebServer("-web", "-webAllowOthers", "-webPort", "9630").start();
        }

        // start meta
        logger.warn("starting meta");

        SpringApplicationBuilder springApplicationBuilder = new SpringApplicationBuilder(MetadataApplication.class);
        springApplicationBuilder.parent(commonContext);
        metaApplicationContext = springApplicationBuilder.run();
        logger.warn("waiting meta");
        // wait meta cluster start
        int metaPort = Integer.parseInt(commonContext.getEnvironment().getProperty(META_HTTP_SERVER_PORT, "9615"));
        waitClusterStart(serverList, metaPort);

        openPush(serverList.stream().findFirst().get(), metaPort);

        // start data
        logger.warn("starting data");
        dataApplicationContext = new SpringApplicationBuilder(DataStoreApplication.class).parent(commonContext).run();

        logger.warn("waiting data");
        // wait data cluster start
        waitClusterStart(serverList, Integer.parseInt(commonContext.getEnvironment().getProperty(DATA_HTTP_SERVER_PORT, "9622")));

        // start session
        logger.warn("starting session");
        sessionApplicationContext = new SpringApplicationBuilder(ClientInterfaceApplication.class).parent(commonContext).run();

        // wait session cluster start
        logger.warn("waiting session");
        waitClusterStart(serverList, Integer.parseInt(commonContext.getEnvironment().getProperty(SESSION_HTTP_SERVER_PORT, "9603")));
    }

    public static void stop() {
        if (sessionApplicationContext != null) {
            sessionApplicationContext.getBean("sessionServerBootstrap", io.kiki.sba.registry.server.clientInterface.bootstrap.Bootstrap.class).destroy();
        }

        if (dataApplicationContext != null) {
            dataApplicationContext.getBean("dataServerBootstrap", Bootstrap.class).destroy();
        }

        if (metaApplicationContext != null) {
            metaApplicationContext.getBean("metaServerBootstrap", MetaServerBootstrap.class).destroy();
        }
    }

    private static void waitClusterStart(Collection<String> serverList, int httpPort) throws Exception {
        for (String serverAddress : serverList) {
            for (int i = 0; i < 100; i++) {
                if (nodeHealthCheck(serverAddress, httpPort)) {
                    logger.info("{}:{} health check success.", serverAddress, httpPort);
                    return;
                }
                logger.error("{}:{} health check failed.", serverAddress, httpPort);
                Thread.sleep(1000);
            }
        }
        Runtime.getRuntime().halt(-1);
    }

    private static boolean nodeHealthCheck(String serverAddress, int httpPort) {
        Response resp = null;
        try {
            HttpClient httpClient = HttpClient.getInstance();
            Channel channel = httpClient.connect(new Url(serverAddress, httpPort));
            logger.info("{}:{} health check", serverAddress, httpPort);
            resp = channel.getWebTarget().path("health/check").request(APPLICATION_JSON).get(Response.class);
            return resp.isSuccess();
        } catch (Throwable t) {
            logger.error("{}:{} health check failed. {}", serverAddress, httpPort, resp, t);
            return false;
        }
    }

    private static void createTables(ConfigurableApplicationContext commonContext) throws ClassNotFoundException, SQLException, IOException {
        String driver = commonContext.getEnvironment().getProperty("jdbc.driverClassName", H2_DRIVER);
        Class.forName(driver);
        String url = commonContext.getEnvironment().getProperty("jdbc.url");
        String username = commonContext.getEnvironment().getProperty("jdbc.username");
        String password = commonContext.getEnvironment().getProperty("jdbc.password");
        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            executeSqlScript(connection, readFileAsString("sql/h2/create_table.sql"));
        }
    }

    public static String readFileAsString(String fileName) throws IOException {

        try (InputStream ins = getFileInputStream(fileName)) {
            String fileContent = IOUtils.toString(ins);
            return fileContent;
        }
    }

    public static InputStream getFileInputStream(String fileName) throws FileNotFoundException {

        return getFileInputStream("./", fileName, FileUtils.class);
    }

    public static InputStream getFileInputStream(String path, String fileName, Class<?> clazz) throws FileNotFoundException {

        File f = null;
        if (path != null) {
            f = new File(path + "/" + fileName);
            if (f.exists()) {
                try {
                    logger.info("[getFileInputStream]{}", f.getAbsolutePath());
                    return new FileInputStream(f);
                } catch (IOException e) {
                    throw new IllegalArgumentException("file start fail:" + f, e);
                }
            }
        }

        // try file
        f = new File(fileName);
        if (f.exists()) {
            try {
                logger.info("[getFileInputStream]{}", f.getAbsolutePath());
                return new FileInputStream(f);
            } catch (IOException e) {
                throw new IllegalArgumentException("file start fail:" + f, e);
            }
        }

        // try classpath
        java.net.URL url = clazz.getResource(fileName);
        if (url == null) {
            url = clazz.getClassLoader().getResource(fileName);
        }
        if (url != null) {
            try {
                logger.info("[start]{}", url);
                return url.openStream();
            } catch (IOException e) {
                throw new IllegalArgumentException("classpath start fail:" + url, e);
            }
        }

        throw new FileNotFoundException(path + "," + fileName);
    }

    protected static void executeSqlScript(Connection connection, String prepareSql) throws SQLException {
        if (StringUtils.isEmpty(prepareSql)) {
            return;
        }
        java.sql.Connection conn = connection;
        PreparedStatement stmt = null;
        try {
            conn.setAutoCommit(false);
            if (!Strings.isEmpty(prepareSql)) {
                for (String sql : prepareSql.split(";")) {
                    logger.debug("[setup][data]{}", sql.trim());
                    stmt = conn.prepareStatement(sql);
                    stmt.executeUpdate();
                }
            }
            conn.commit();

        } catch (Exception ex) {
            logger.error("[SetUpTestDataSource][fail]:", ex);
            if (null != conn) {
                conn.rollback();
            }
        } finally {
            if (null != stmt) {
                stmt.close();
            }
            if (null != conn) {
                conn.setAutoCommit(true);
                conn.close();
            }
        }
    }

    protected static void openPush(String metaAddress, int metaPort) {
        HttpClient httpClient = HttpClient.getInstance();
        Channel channel = httpClient.connect(new Url(metaAddress, metaPort));
        Result result = channel.getWebTarget().path("/stopPushDataSwitch/close").request().get(Result.class);
        if (!result.isSuccess()) {
            throw new RuntimeException(StringFormatter.format("open push failed: {}", result.getMessage()));
        }
    }
}
